
import inspect
import os
import sys

import ab_error
import ab_ids as ai
import ab_utils as au

from ab_exceptions import AofException

# PlxExtra - Extra plug-ins for customization
# PlxTpBinaries - binary plug-ins for security/
PlxTpUnknown = 0x0000
PlxTpConverter = 0x0001
PlxTpFormatter = 0x0002
PlxTpGenerator = 0x0004
PlxTpOrganizer = 0x0008
PlxTpProvider = 0x0010
PlxTpMask = 0x001f

PlxTpExtras = 0x0040
PlxTpBinaries = 0x0080
PlxTpAuto = 0x0100
PlxTpInstalled = 0x0200

PlxStatusReady = 0x1000

PlxAttrMask = 0xfffe00

# plug-in for tree-only
PlxCtxTree = 0x002000
PlxAttrPicture = 0x004000

PlxAttrAllPlugin = 0x000000
PlxAttrSystem = 0x010000
PlxAttrNormal = 0x020000
PlxAttrReinstall = 0x040000
PlxAttrGeneric = 0x080000
PlxAttrInternal = 0x100000
PlxAttrExtend = 0x020000
PlxAttrNoSource = 0x040000

PLX_CATEGORY_INFO = {
    PlxTpConverter: {
        'service': 'cvtService',
        'name': 'Converter',
        'dir': 'Convert',
        'verifier': lambda ii:
            hasattr(ii, 'Execute') or
            (hasattr(ii, 'Convert') and hasattr(ii, 'Validate'))},
    PlxTpFormatter: {
        'service': 'fmtService',
        'name': 'Formatter',
        'dir': 'Format',
        'verifier': lambda ii:
            hasattr(ii, 'Execute') or
            (hasattr(ii, 'Parse') and hasattr(ii, 'Validate'))},
    PlxTpGenerator: {
        'service': 'genService',
        'name': 'Generator',
        'dir': 'Generat',
        'verifier': lambda ii:
            hasattr(ii, 'Execute') or
            (hasattr(ii, 'Generate') and
             (hasattr(ii, 'Validate') or hasattr(ii, 'GetAttribute')))},
    PlxTpOrganizer: {
        'service': 'orgService',
        'name': 'Organizer',
        'dir': 'Organiz',
        'verifier': lambda ii:
            hasattr(ii, 'Execute') or hasattr(ii, 'Transfer')},
    PlxTpProvider: {
        'service': 'prvService',
        'name': 'Provider',
        'dir': 'Provide',
        'verifier': lambda ii:
            hasattr(ii, 'Execute') or hasattr(ii, 'GetReferral')},
    PlxTpAuto: {
        'name': '-AUTO-',
        'service': 'Service',
        'mask': ~PlxTpAuto,
        'attribute': PlxAttrInternal,
        'verifier': lambda ii: True},
    PlxTpInstalled: {
        'name': 'Installed',
        'attribute': PlxAttrInternal,
        'support': (PlxTpConverter | PlxTpFormatter | PlxTpGenerator |
                    PlxTpProvider | PlxTpOrganizer),
        'dir': 'installed'},
    PlxTpExtras: {
        'name': 'Extras',
        'attribute': PlxAttrInternal,
        'support': (PlxTpConverter | PlxTpFormatter | PlxTpGenerator |
                    PlxTpProvider | PlxTpOrganizer)},
    PlxTpBinaries: {
        'name': 'Binaries',
        'attribute': PlxAttrInternal,
        'support': (PlxTpConverter | PlxTpFormatter | PlxTpGenerator |
                    PlxTpProvider | PlxTpOrganizer),
        'dir': 'Binaries'},
}


class PlxCategory:
    def __init__(self):
        self.cat = PLX_CATEGORY_INFO

    def getCategories(self, all=False):
        if all:
            return self.cat.keys()
        else:
            return filter(lambda x: self.cat[x].get('attribute', 0)
                          != PlxAttrInternal, self.cat.keys())

    def getCategoryWithPath(self, path):
        if path != '':
            for c in self.cat:
                if self.cat[c].get('dir', '').lower() == path.lower():
                    return c

        return PlxTpUnknown

    def get(self, key):
        return self.cat.get(key)

    def getNameWithId(self, key):
        cat = self.cat.get(key)
        if cat:
            return cat.get('name')

        return None

    def getPathWithId(self, key):
        cat = self.cat.get(key)
        if cat is not None:
            return cat.get('dir')

        return None

# Defined labels
L_AOF = '__aof__'
L_CFG = '__configs__'
L_CRE = '__creator__'
L_DOC = '__doc__'
L_EXT = '__extension__'
L_OPT = '__options__'
L_REF = '__referral__'
L_VER = '__version__'


def _setPredefinition(inst, kws):
    for p in (L_AOF, L_DOC, L_EXT, L_OPT, L_VER, L_CFG):
        if p in kws and not hasattr(inst, p):
            setattr(inst, p, kws.get(p))


class PlxElement:
    def __init__(self, mgr, name, sname, instance, category,
                 attribute, **args):
        self.plx = mgr
        self.name = name
        self.sname = sname

        self.category = category
        self.instance = instance
        self.attribute = attribute

        # make sure L_AOF is existent
        if L_AOF not in args:
            args[L_AOF] = (name, sname)

        self.args = args
        self.mtime = self.__modifiedTime()
        self.exts = self.__extensions(args.get(L_EXT))

        self.plx.updateConfigs(self.name, self.sname, args.get(L_CFG))

    def __str__(self):
        return '<<PlxElement: %r> name:%s, sname: %s, category=%x, \
               attribute=%x, mtime=%d>' % (self, self.name, self.sname,
                                           self.category, self.attribute,
                                           self.mtime)

    def __extensions(self, extensions):
        ret = dict()
        for description, exts in extensions or list():
            if description not in ret:
                ret[description] = list()

            for e in exts:
                if e not in ret[description]:
                    ret[description].append(e)

        return ret

    def __modifiedTime(self):
        mtime = 0

        filename = self.getFilename()
        if filename and os.path.exists(filename):
            tm = os.stat(filename)
            mtime = tm.st_mtime

        return mtime

    def isModified(self):
        return self.mtime != self.__modifiedTime()

    def isEnabled(self):
        if not self.plx.cf.readKey(ai.CfgParaPlxEnableAll):
            cat_enabled = self.plx.cf.readKey(ai.CfgParaPlxEnabledCategory)
            for c in cat_enabled.keys():
                if (c & self.category) != 0:
                    break
            else:
                plxs_enabled = self.plx.cf.readKey(ai.CfgParaPlxEnabledList)
                for e in plxs_enabled.values():
                    if self.name in e.split(','):
                        break
                else:
                    return False

        return True

    def getInstance(self):
        if inspect.isclass(self.instance):
            inst = self.instance()
        else:  # InstanceType
            inst = self.instance

        _setPredefinition(inst, self.args)
        return inst

    def getInterface(self, referral):
        for ref in self.args.get(L_REF, list()):
            if ref[0].lower() == referral.lower():
                return ref[1]

        return None

    def getName(self):
        return self.name

    def getShortname(self):
        return self.sname

    def getFilename(self):
        return self.args.get('filename')

    def getExtensions(self):
        return self.exts

    def getAttribute(self):
        return self.attribute

    def getCreator(self):
        return self.args.get(L_CRE)

    def getOption(self):
        return self.args.get(L_OPT)

    def getCategory(self, singleton=False):
        if singleton:
            k = 1
            while k < PlxTpMask:
                if k & self.category:
                    inst = self.plx.category.get(k)
                    if inst:
                        return k
                k *= 2

            return PlxTpUnknown

        return self.category

    def getInfo(self, dummy=False):
        return dict({
            'name': self.name,
            'shortname': self.sname,
            'attribute': self.attribute,
            'version': self.args.get(L_VER, ''),
            'filename': self.args.get('filename', ''),
            'message': self.args.get(L_DOC,
                                     'No information for the plug-in'),
            'referrals': [e[0] for e in self.args.get(L_REF, list())],
            'filename': self.getFilename()
        })


def _addIfNotNull(d, key, module, default=None):
    value = getattr(module, key, default)
    if value is not None:
        d[key] = value

    return value


class PluginFactor:
    def __init__(self, manager):
        self.manager = manager
        self.log = self.manager.log

        self.sname = list()

    def __shortName(self, name):
        """ Generates a new shortname which won't be duplicated with the
        registered one. The short name won't be constant if it's not specified.
        """
        LENGTH = 3

        nname = ''
        w = name.split()
        if len(w) >= LENGTH:
            # use the first letter from each word
            for k in range(LENGTH):
                nname += str(w[k][0])
        else:
            nname = ''.join(w)
            if len(nname) < LENGTH:
                nname += 'AAAA'[:LENGTH - len(nname)]
            else:
                nname = nname[:LENGTH]

        of, nname = LENGTH - 1, nname.upper()
        # if the value is existent in hash, adjust the value
        while nname in self.sname and of > -1:
            order = ord(nname[of])
            if order + 1 > ord('Z'):
                v = 'A'
            else:
                v = chr(order + 1)

            nname = nname[:of] + v + nname[of + 1:]
            # with the solution, it can support 26*26*26 short name totally
            if v == 'A':
                of -= 1

        self.sname.append(nname)
        return nname

    def __loadInstance(self, info, tp, instance_, name=None, sname=None,
                       filename=None, **kws):

        def _convertAttribute(attribute):
            value = 0
            if isinstance(attribute, (str, unicode)):
                for a in attribute.split(','):
                    if a == 'converter':
                        value |= PlxTpConverter
                    elif a == 'formatter':
                        value |= PlxTpFormatter
                    elif a == 'generator':
                        value |= PlxTpGenerator
                    elif a == 'organizer':
                        value |= PlxTpOrganizer
                    elif a == 'provider':
                        value |= PlxTpProvider
                    elif a == 'system':
                        value |= PlxAttrSystem
                    elif a == 'non-source':
                        value |= PlxAttrNoSource
                    elif a == 'ctx-tree':
                        value |= PlxCtxTree
                    elif a == 'picture':
                        value |= PlxAttrPicture
            elif isinstance(attribute, (int, long)):
                # obsolete and will be removed later
                value = attribute

            return value

        ret = None
        if isinstance(instance_, (list, tuple)) and len(instance_) > 1:
            instance, attribute = instance_[0], _convertAttribute(instance_[1])
        else:
            instance, attribute = instance_, 0

        if inspect.isclass(instance):
            inst = instance()
        else:
            inst = instance

        # handle interface 'GetName'
        _addIfNotNull(
            kws, L_AOF, inst,
            au.getAttrFromMethods(inst, ('get_name',),
                                  getattr(inst, L_AOF, name)))

        nm = kws.get(L_AOF)
        if isinstance(nm, (list, tuple)) and len(nm) > 1:
            name, sname = nm
        elif isinstance(nm, (str, unicode)):
            name = nm
        else:
            raise AofException(name, 'module.GetName return invalid values')

        if name is None:
            raise AofException(filename, 'module has no name')

        # create the shortname
        if sname in self.sname:
            self.log.warn('%s: short name %s is defined' % (name, sname))

        if sname is None or sname in self.sname:
            sname = self.__shortName(name)

        # get the extension
        _addIfNotNull(
            kws, L_EXT, inst,
            au.getAttrFromMethods(inst, ('get_exts',)))

        verifier = info.get('verifier', None)
        if verifier is None or (verifier and verifier(inst)):
            #   1. GetInfo
            #   2. instance __doc__
            #   3. file __doc__
            _addIfNotNull(
                kws, L_DOC, inst,
                au.getAttrFromMethods(inst, ('get_info',)))
            _addIfNotNull(kws, L_VER, inst)
            _addIfNotNull(
                kws, L_REF, inst,
                au.getAttrFromMethods(inst, ('get_referral',),
                                      getattr(inst, L_REF, list())))
            _addIfNotNull(
                kws, L_CRE, inst,
                au.getAttrFromMethods(inst, ('get_creator',),
                                      getattr(inst, L_CRE, None)))

            attribute |= au.getAttrFromMethods(inst, ('get_attribute',), 0)
            category = (tp | attribute) & info.get('mask', ~PlxAttrMask)
            # add to the plug-in list
            ret = PlxElement(self.manager,
                             name, sname,
                             instance, category,
                             attribute,
                             filename=filename,
                             **kws)

        return ret

    def install(self, module, filename, tryrun=False):
        ret = True
        kws = dict()

        reti = list()
        msname = None
        mname = _addIfNotNull(kws, L_AOF, module)
        if mname:
            if isinstance(mname, (list, tuple)) and len(mname) == 2:
                mname, msname = mname
            elif not isinstance(mname, (str, unicode)) or len(mname) != 1:
                raise AofException("module.__aof__ isn't correct")

        _addIfNotNull(kws, L_DOC, module)
        _addIfNotNull(kws, L_VER, module)
        _addIfNotNull(kws, L_OPT, module)
        _addIfNotNull(kws, L_CFG, module)
        # load file extensions
        extension = _addIfNotNull(kws, L_EXT, module)
        if extension and not tryrun:
            self.manager.addExtension(extension)

        ci = self.manager.getCategory()
        # loop to enable the mixture of the several plug-ins inside a file
        for t in ci.getCategories(all=True):
            info = ci.get(t)
            service = info.get('service')

            if not service or not hasattr(module, service):
                continue

            # get the list of instance or class
            insts = getattr(module, service)()

            # support multiple instances from a plug-in
            if isinstance(insts, (list, tuple)):
                # it need distinguish the return is a list of plug-in instances
                # or an instance with its attribute
                if len(insts) == 2 and \
                        isinstance(insts[1], (int, long, str, unicode)):
                    ri = self.__loadInstance(info, t, insts, mname, msname,
                                             filename, **kws)
                else:
                    for ii in insts:
                        ri = self.__loadInstance(info, t, ii, mname, msname,
                                                 filename, **kws)
                        if ri:
                            reti.append(ri)
                            if not tryrun:
                                self.manager.addExtension(ri.getExtensions())
                            ri = None
                        else:
                            ret = False
                            break
            else:
                ri = self.__loadInstance(info, t, (insts, 0), mname, msname,
                                         filename, **kws)

            if ri:
                reti.append(ri)
                if not tryrun:
                    self.manager.addExtension(ri.getExtensions())
            elif len(reti) == 0:
                ret = False

            if not ret:
                break

        return ret, reti


class PluginManager:

    # constant
    ExtAllFiles = 0x00
    ExtPython = 0x01
    ExtPlainText = 0x02
    ExtBinaryText = 0x03
    ExtLogFile = 0x04

    def __init__(self, cf):
        self.cf = cf
        self.plist = dict()
        self.configs = dict()

        # Get object instances
        self.log = self.cf.readKey(ai.CfgParaLogObject)

        self.exts = {}
        self.factor = PluginFactor(self)
        self.category = PlxCategory()

    def addExtension(self, extension):
        if isinstance(extension, dict):
            for description, exts in extension.items():
                if description not in self.exts:
                    self.exts[description] = list()

                for e in exts:
                    if e not in self.exts[description]:
                        self.exts[description].append(e)

    def getExtension(self, name=None, *args):
        def _buildExtString(exts_):
            ret = self.cf.readKey(ai.CfgParaPlatform).getAllFilesExtension()

            names = exts_.keys()
            names.sort()
            for e in names:
                ext = self.exts[e]
                osl = ';'.join(ext)
                pre = ' ,'.join(ext)
                ret += '|%s (%s)|%s' % (e, pre, osl)

            return ret

        exts = dict()
        if name is not None:
            item = self.getElement(name)
            if item is not None:
                exts = item.getExtensions()
        else:
            exts = self.exts

        return _buildExtString(exts)

    def installModule(self, module, filename=None, tryrun=False):
        names = list()

        try:
            ret, insts = self.factor.install(module, filename, tryrun=tryrun)
        except:
            ret = False
            self.log.error("Except[install]: %s <%s>" %
                           (filename, repr(module)))
            self.log.error(ab_error.printExcInfo(sys.exc_info()))

        if ret:
            # add the instances to the lists
            for inst in insts:
                # output the warning for plug-in overlap
                if inst.name in self.plist:
                    filename = inst.getFilename() or ' '
                    self.log.warn('plug-in %s is existent from file%s, '
                                  'uninstall need be invoked first'
                                  % (inst.name, filename))

                if not tryrun:
                    self.plist[inst.name] = inst

                names.append(inst.name)

        return ret, names

    def installFile(self, filename, tryrun=False):
        ret, names = True, list()

        name = os.path.splitext(os.path.basename(filename))[0]
        path = os.path.dirname(filename)
        sys.path.append(path)

        # load the module
        try:
            module = __import__(name, globals())
            reload(module)
        except:
            ret = False
            self.log.error("Except[installFile]: %s" % filename)
            self.log.error(ab_error.printExcInfo(sys.exc_info()))

        if ret:
            ret, names = self.installModule(module, filename, tryrun=tryrun)

        sys.path.pop(-1)

        return ret, names

    def updateConfigs(self, name, sname, options):
        if options is not None:
            self.configs[name] = options
            self.cf.installPlxConfig(name, sname, options)

    def getConfigs(self):
        return self.configs

    def getCreators(self):
        ret = list()
        for name, instance in self.plist.items():
            creator = instance.getCreator()
            if creator is not None:
                ret.append((name, creator))

        ret.sort(lambda a, b: cmp(a[0]. b[0]))

        return ret

    def getModified(self):
        ret = list()

        for _, instance in self.plist.items():
            if instance.isModified():
                name = instance.getFilename()
                if name not in ret:
                    ret.append(name)

        return ret

    def getPlxList(self, attr=PlxAttrAllPlugin, required_all=False):
        # get the categorized plug-ins first
        att = attr & (~PlxAttrMask)
        plxs = filter(lambda c: (self.plist[c].category & att) == att,
                      self.plist.keys())

        att = attr & PlxAttrMask
        if attr == PlxAttrNormal:
            # filter the ones with the attribute system
            ret = filter(lambda c: (self.plist[c].attribute
                                    & PlxAttrSystem) != PlxAttrSystem, plxs)
        elif attr == PlxAttrGeneric:
            # filter the ones with the attribute internal
            ret = filter(lambda c: (self.plist[c].attribute &
                                    PlxAttrInternal) != PlxAttrInternal, plxs)
        elif attr != PlxAttrAllPlugin:
            ret = filter(lambda c: (self.plist[c].attribute & att)
                         == att, plxs)
        else:
            ret = plxs

        # do filter the enabled attribute
        if not required_all and not self.cf.readKey(ai.CfgParaPlxEnableAll):
            cat_enabled = self.cf.readKey(ai.CfgParaPlxEnabledCategory)
            plxs_enabled = self.cf.readKey(ai.CfgParaPlxEnabledList)
            for plx in plxs_enabled or dict():
                plxs_enabled[plx] = plxs_enabled[plx].split(',')

            for p in ret[:]:
                c = self.plist[p].category
                for ce in cat_enabled.keys():
                    if (c & ce) != 0:
                        break
                else:
                    for e in plxs_enabled.values():
                        if p in e:
                            break
                    else:
                        ret.remove(p)

        ret.sort()
        return ret

    def getFilteredPlxList(self, plxs, attr):
        ret = list()

        for p in plxs or list():
            if p in self.plist and (self.plist[p].attribute & attr):
                continue

            ret.append(p)

        return ret

    def getPlxLists(self, categories,
                    attr=PlxAttrAllPlugin, required_all=False):
        ret = dict()

        for c in categories:
            ret[c] = self.getPlxList(c | attr, required_all)

        return ret

    def getCategory(self):
        return self.category

    def getElement(self, name):
        if name in self.plist:
            return self.plist[name]
        else:
            for p in self.plist:
                if self.plist[p].getShortname() == name:
                    return self.plist[p]

        return None

    def getInstance(self, name, enabled=False):
        item = self.getElement(name)
        if item is not None:
            if enabled or item.isEnabled():
                return item.getInstance()

        return None
